Conversation
This adds support for using a custom BIO method for performing SSL/TLS I/O through a Ruby IO instance (normally the underlying socket). Alternatively, a pair of read and write procs may be used for performing the I/O.
|
I have a WIP branch in #736 which takes a similar approach of providing a custom
Wouldn't that convert all methods on
Some changes will be necessary there for proper error handling. Because Ruby exceptions are implemented using If the underlying BIO raises an exception, we must temporarily catch it, allow OpenSSL to clean up its internal state, wait for |
Indeed, this implementation would change the behavior to blocking, at least when using an IO-like object for the underlying BIO. However, for the custom BIO option (providing a read and a write proc), I have I understand the importance of supporting the non-blocking API. At the same time, having everything implemented as non-blocking is preventing other use cases, such as better integration with a fiber scheduler, or using io_uring for the actual I/O. Allowing a blocking BIO method is also an opportunity for achieving better performance, especially in conjunction with io_uring.
I have included a test case for raising an exception from within the BIO method, that seems to be working correctly. However, this may need more testing. https://github.com/ruby/openssl/pull/1000/changes#diff-9b8fe96fbb75f887aaa0cb69b982c886e041707496f37d8d6a5b443d92a5c347R2460
|
|
I'm closing this PR as it doesn't provide a good solution for supporting non-blocking I/O. |
|
I don't think we should throw this out just because it doesn't support non-blocking. If the only use case for non-blocking IO is users implementing timeouts, that's already better handled by As an aside, I personally think |
|
I agree the non-blocking API certainly complicates things but now it's just something we need to deal with. Maybe a solution would be to disable the non-blocking methods when a custom BIO is used? I'm open to suggestions, it's not clear to me how to proceed with this PR. @HoneyryderChuck since you're behind the original issue #731 would you like to give your take on this PR? Can this solve your specific use case? |
|
@noteflakes sorry for the late reply, haven't had much time to come back to the pending openssl related threads. I like that your proposal is quite simple. Still, it seems that, in order to get non-blocking behaviour, I'd have to run on top of a fiber scheduler that hooks on the write/read blocking APIs, and that's unfortunately not the lowest common denominator in ruby. My original request had nothing to do with that though, I justed wanted to enable HTTPS proxy mode in I still think that your angle of allowing a "bring your own BIO method" could be something worthwhile, particularly because in your case, you're targeting a liburing based fiber scheduler, so ideally openssl should accomodate that optionally for the cases of openssl sockets used under a thread running your scheduler; shipping your own BIO method on top of your fiber scheduler like a way to address it.
I can understand the argument, but I think it's too late. Most (all?) network libs in ruby use the nonblocking variants. I haven't made a lot of use of FWIW I'd like an openssl library built with more ruby, with a more direct layer of integration with libopenssl and friends. Most of the public interface maps to C extensions, and there's a lot of "callbacks" to CRuby API in between calls to the SSL API, which leaves some optimizations on the table, or reusing code in jruby impossible, but also make things such as implementing your own BIO method quite cumbersome. That's why I've trying to reimplement (most of) the ASN1 module in plain Ruby (something I have to get back to), and certainly something that I believe makes @rhenium patch much harder to validate (there's a lot of CRuby API in that BIO method, which makes it hard to consider the edge cases around error handling). Just my thoughts. |
Fortunately this is pretty easy, and that's actually what I spent the last couple of days doing:
Maybe this got lost between the comments, but this PR already includes support for non-blocking I/O when using a custom BIO. 6c1db60
Regarding error handling, please correct me if I'm wrong, but it seems to me that this might actually be a non-issue, at least in most circumstances. If any callback raises an exception without rescuing it, or if any I/O method fails with a |
You can already do this using a pipe, just in case you wanted something that's working today.
It works fine with or without a fiber scheduler. |
This PR adds support for using a custom BIO method for performing SSL/TLS I/O. It is an alternative to #736 and a possible fix for #731.
Summary
Currently, the openssl gem uses a socket BIO (over non-blocking sockets) that bypasses the Ruby I/O layer, except for calling
io_wait_readable/io_wait_writableto wait for I/O readiness. This prevents or makes it difficult to do encrypted I/O over alternative transports such as proxy connections (#731) or virtual sockets, for example in a testing situation.I've also been looking at providing a better way to integrate the openssl gem with a fiber scheduler and specifically being able to use it in conjunction with a low-level API for performing I/O using io_uring that I'm developing.
The aim of this PR is to provide a minimal API that allows setting a custom BIO method that either uses the stock
IO#readandIO#writemethods to perform I/O, or alternatively use custom procs to perform read and write operations, which will allow complete freedom for performing I/O using a low-level API, a proxy connection or any virtual interface.The proposed solution is based on the following design principles:
SSLSocketI/O methods inossl_ssl.c:#read,#write,#connect,#acceptetc.API
We add two methods:
SSLSocket#bio_method: get the socket's BIO method.SSLSocket#bio_method=: set the socket's BIO method.The setter method accepts the following:
nil: use the default socket BIOIO: use the given IO instance to perform I/O, via its#readand#writemethods. This will usually be the underlying socket object. Example usage:Object: use the given object to perform I/O, using the same interface as for an IO. Example usage:[read_proc, write_proc]: use the given pair of read/write procs to perform IO.The read proc takes an
IO::Bufferand a maximum read length, and should return the number of bytes read. The write proc takes anIO::Bufferand a write length, and should return the number of bytes written. Example usage:Implementation
Since we're just changing the BIO associated with the
SSLSocketinstance, we don't need to touch the I/O implementation in functions such asossl_ssl_read_internal. The custom BIO interface will never returnSSL_ERROR_WANT_READorSSL_ERROR_WANT_WRITE, and thus the I/O operation will complete immediately after the call to e.g.SSL_read.Since the custom BIO read and write hooks receive raw
char *buffers, we need to pass the buffer as eitherString(in the case ofIO/Objectmethod), orIO::Buffer(in the case of read/write procs). The advantage of using aIO::Bufferis that there's no need to copy data between the raw buffer and the string (or vice versa). Hopefully, in the future,IO#readandIO#writewould be able to accept anIO::Bufferas well asStringas buffer.Performance
A preliminary benchmark (source) shows a significant advantage to using a custom BIO method. This benchmarks measures the performance of the default socket BIO, the
IOcustom BIO method, and a custom BIO method using the UringMachine low-level API.Of course, the performance implications need to be investigated more thoroughly and may vary by OS, OpenSSL version, machine and network setup etc, and also concurrency.
OpenSSL and Ruby Compatibility
The implementation depends on the availability of
BIO_meth_newand associated functions, which were added in OpenSSL 1.1.0.The implementation also depends on the availability of the
IO::Bufferc API, namelyrb_io_buffer_new(available since Ruby 3.1) andrb_io_buffer_free_locked(available since Ruby 3.3).Future work
bio_methodkwarg toSSLSocket.new/SSLSocket.open(see also Addsync_closekwarg toSSLSocket.new#996).#readand#writemethods.cc @HoneyryderChuck @ioquatix @rhenium